﻿\subsection{操作系统内核}

对于 CPU 而言，操作系统内核与一般的应用程序唯一的区别就在于二者运行时所处的特权级不同，因而操作系统内核能够使用像 \lstinline{sti} 这样的特权指令。
最简单的操作系统内核就是一个不断执行的循环体。
在本节中，引导加载器将实现对这个最简单的操作系统内核的装载，在随后的章节会逐步地完善操作系统内核。

\begin{minted}{c}
/* The Simplest OS Kernel */
void SimpleMain()
{
    while (TRUE)
    {
        __asm hlt /* hlt 指令减少 CPU 占用率 */
    }
}
\end{minted}

\subsubsection{操作系统内核的编译、链接过程}

从项目可维护性的角度考虑，构建内核时的编译、链接应单独拆分。
本项目将使用 LLVM-Clang 作为 C 语言编译器，LLVM-LLD 作为中间文件的链接器，当需要单独编写汇编文件时，使用 NASM 作为汇编器。

对于一份 C 源代码文件 \lstinline{example.c}，LLVM-Clang 编译器前端会将源程序编译为 LLVM IR（LLVM Intermediate Representation，LLVM 中间表示）\lstinline{example.ir} ，编译器后端将 LLVM IR 翻译成 x86-32 的指令，生成具有特定二进制格式的可重定位文件 \lstinline{example.obj}。
\lstinline{example.obj} 文件中缺少某些声明符号的定义，在随后的若干次部分链接（Partial Linking，或称可重定位链接，Relocatable Linking）\cite{ld-doc} 过程中，这些符号的定义会在链入的可重定位文件（如库文件）中找到。
当可重定位符号的定义全部补全后，由链接器赋予这些符号一个最终的装载地址（也可手动指定），形成可执行文件（图 \ref{COMPILE_LINK_LOAD}）。
根据该装载地址将可执行文件装入内存，由 CPU 解释执行。
上述过程中涉及到的可重定位文件和可执行文件都遵守固定的二进制格式规范，如：PE 格式、ELF 格式、Mach-O 格式。
这些二进制格式规范用于辅助程序链接和装载，本项目使用 ELF 作为二进制文件格式规范。

\begin{figure}[h!]
    \Centering
    \caption{LLVM 工具链编译、链接 C 源代码文件的流程}
    \label{COMPILE_LINK_LOAD}
    \includegraphics[scale=0.6]{build/Paper/Assets/Compile&Link&Load.pdf}
\end{figure}

对不同编程语言编写的源程序，编译器将其编译成遵守某一种二进制格式规范的可重定位文件，这样便实现了不同编程语言在二进制层面的兼容。
最终由链接器解析可重定位文件中的符号，形成可执行文件。
本操作系统设计过程中，采用单独编译、多次可重定位链接的方式构建项目。
等到各个模块都链接完毕，再使用一条链接的命令完成绝对链接，赋予各个可重定位符号以绝对地址，并指明程序的入口。
单独编译、多级链接的好处在于只需要对更新的模块进行单独编译后重新链接，而无需每次都重新编译、链接全部的模块，提高了项目构建速度和开发效率。
编译后的操作系统文件映像通过 \lstinline{dd} 工具写入磁盘，由引导加载器负责将其装载到内存中。

在操作系统内核被装载启动后，操作系统内核并非一无所有。
引导加载器已经为操作系统内核准备了一块区域存放由 BIOS 中断获取到的物理内存信息以及初始的页目录和页表。
操作系统应该需要得知这些内存数据存放于何处，然后才能使用它们。
一种方案在操作系统中定义一个指针，然后通过强制类型转换为其赋予地址。
该方案存在两点缺陷：
其一，当数据位置发生变动时，需要依次在源文件中找到这些指针的定义并修改；
其二，像内存地址图这样的数据本身实际上可以看做是一个结构体元组，其元素的个数是确定的，若采用指针的方式对其进行引用，则会丢失这样的类型信息。
要解决此问题，可使用\textbf{链接脚本（Linker Script）}为这些有意义的内存块创建符号，并赋予其绝对地址。

仍以内存地址图为例，在使用链接脚本定址时，在 C 语言中只需要做如下声明：

\begin{minted}[breakautoindent=true, breaklines]{cpp}
extern AddressRangeDescriptor g_MemoryAddressMap[64]; /* 内存地址图 */
\end{minted}

而后，创建一个链接脚本文件专门用于指定这些符号的绝对地址。其中，为 \lstinline{g_MemoryAddressMap} 符号做如下的记录：

\begin{minted}[breakautoindent=true, breaklines]{cpp}
SECTIONS
{
    .data :
    {
        /* 以下符号指定为绝对符号，在可重定位文件中作为 ABS 类型的符号存储 */
        g_MemoryAddressMap = ABSOLUTE(0xC0062000);
        /* ... */
    }
}
\end{minted}

链接脚本与一般的目标文件一同传递给链接器作为输入。
链接完成后，C 语言程序中对符号 \lstinline{g_MemoryAddressMap} 的引用均能被正确解析到绝对地址 \lstinline{0xC006'2000}。

\subsubsection{加载操作系统文件映像}

操作系统内核经编译、链接形成可执行文件后，以文件映像的形态存在于磁盘中，需要由引导加载器从硬盘读取操作系统内核并装入内存执行。
引导加载器此时已经进入 32 位保护模式，无法使用 16 位实模式下的 BIOS 中断功能，因而需要额外编写一个磁盘驱动程序。

CPU 通过总线与外围设备连接，IA-32 指令集提供了 \lstinline{in} 和 \lstinline{out} 两条指令，用于向连接在总线上的外围设备端口传送指令或数据。
硬件从端口接收到的指令、数据存放于硬件内的寄存器中，在接到指令或数据硬件设备后作出相应反馈，完成某项约定的功能。
以这样的方式指挥硬件设备工作的程序被称作驱动程序（Driver），驱动程序的本质就是向硬件的端口写入指令或数据，指挥、协调硬件，将硬件设备对上层的程序抽象为一组编程接口。
ATAPI （Advanced Technology Attachment Packet Interface）定义了硬盘设备的接口规范，根据 ATAPI 编写硬盘驱动程序，便可将操作系统镜像文件读入内存。

ATA 兼容（ATA Compliant）设备介入计算机后分为主（Master）硬盘和从（Slave）硬盘 \footnote{ 二者并没有实际的主从关系，主从仅用于对两块硬盘设备的区分。
}，分别接在 CPU 的 Primary Bus 和 Secondary Bus 上，CPU 通过写端口指令对硬盘中的寄存器进行操作，进而控制硬盘设备行为，这种控制硬盘的模型被称作 PIO 模式（Programming I/O Mode）。
硬盘对外提供两组寄存器，分为称作 I/O 寄存器和控制寄存器。
对于主硬盘，其 I/O 寄存器组的基址 \lstinline{IO_BASE} 为 \lstinline{0x1F0}，控制寄存器组的基址 \lstinline{CTRL_BASE} 为 \lstinline{0x3F6}，从设备的这两个值分别为 \lstinline{0x370} 和 \lstinline{0x376}。
根据表 \ref{ATA_PIO} 中给定的寄存器地址，就可通过 \lstinline{in} 或 \lstinline{out} 控制这些寄存器。
有关错误码、指令格式等内容具体规范定义在 ATA 手册中 \cite{atapi}。

\begin{figure}[h!]
    \Centering
    \captionsetup{type=table}
    \caption{ATA 磁盘设备 PIO 模式}
    \label{ATA_PIO}
    \includegraphics[scale=0.65]{build/Paper/Assets/ATA-PIO-Mode.pdf}
\end{figure}

读写 ATA 设备时需要指定磁盘扇区的地址，扇区地址格式用于定位扇区在磁盘中的位置，扇区地址格式共有两种：CHS（Cylinder-Head-Sector）和 LBA（Logical Block Address）。
其中，CHS 模式在上一节编写主引导记录时已经提及过，此处不再赘述。
CHS 的缺点是显然的，若不查阅手册或某些特定的命令查询硬盘信息，程序员并不知道一块磁盘有多少柱面、扇区、磁头。
其次，CHS 地址还需要额外考虑进位的问题，且扇区号由于规定从 1 开始，编程时还需要额外处理。
ATA 中定义了另一种更加易用的扇区地址格式，它将磁盘抽象为一个逻辑上连续的块设备，块大小为 \Bytes{512}，逻辑块从 0 开始连续编址。
从 LBA 到实际用于访问磁盘的 CHS 地址的转换工作由磁盘设备自身完成。
LBA 依据设备容量不同可细分为 LBA28 （28 位地址）和 LBA48（48 位地址），后者可用于大容量磁盘的扇区寻址。
在指定磁盘 LBA 地址后，在数据端口准备好一个字的数据，发出读写命令即可向磁盘写入数据。

PIO 模式下，CPU 直接控制对磁盘的读写，CPU 一次最只能磁盘的数据寄存器中取出 1 个字，且磁盘的存取速度远远落后于 CPU 几个数量级。
因此 ATA 规范中还提供了另一种磁盘存取方式 DMA （Direct Memory Access），在内存和磁盘之间构建一条单独的数据通路，在磁盘数据写入内存时，CPU 可以转而处理其他工作。
不过，到目前为止，我们尚处在引导加载器中，连操作系统都不存在。
因此，CPU 除了读取磁盘，实在是没有工作可做，故本节中仍基于 PIO 模式实现读取磁盘的简易驱动程序。
与上一节相仿，这里仍然采用一次读入一个扇区的方式，先实现读取单个扇区的函数，再通过循环读取连续的几个扇区，这样，在读取某个扇区出错时只需要重试读取当前扇区即可。

\subsubsection{装载操作系统文件映像}

在将 ELF 格式的操作系统文件映像读入内存后，还需要将操作系统文件映像中存储的各个段正确地展开到内存中，这样操作系统才能够在 CPU 上正确运行。

操作系统文件映像具有上文中提到的二进制格式。
下面以本项目使用的 ELF32 格式 \cite{elf-doc} 为例进行简单介绍（图 \ref{ELF_FORMAT}）。

ELF 格式的二进制文件可粗略划分为可重定位文件（尚未完成最终链接）和可执行文件（已完成最终链接，所有符号均已完成重定位）两类，即链接视图和执行视图。
链接视图和执行视图都具有固定大小的 ELF 头部。
链接视图中，还包若干节构成的节表（Section Table）以及这些节的摘要信息节头（Section Header）构成的节头表（Section Header Table）。
在使用汇编语言编写程序时，常常要将程序划分为若干个节，汇编器将汇编程序编译为 ELF 格式的可重定位文件后，这些这些节的指令和数据就存放在节表中，每个节的摘要信息都由节头表中的对应表项描述，编译器正是根据节头表及节表中的内容完成链接工作。
链接完成后，若干个节被合并为一个程序段（Program Segment）
\footnote{
    在 NASM 等汇编语言中，节（Section）与段（Segment）被视作可互换的等价概念。
    本文中的 \textbf{程序段} 与汇编语言中的段（节的别名）并不是一回事，而是若干个节在链接后的产物。
    本文在讨论编译与链接过程时严格使用“节”和“程序段”两个名词区分这二者以避免混淆。
    编译、链接过程中的程序段与 x86 的内存分段机制有一定关联，但程序段与 x86 CPU 的分段仍是两个不同的概念。
    前者是对程序逻辑功能的划分，后者则是 x86 体系结构下的硬件接口特性。
}
，原来的节表此时就变成了程序段表（Program Sengment Table），每个程序段的摘要信息都由存放在程序段头（Program Segment Header）\footnote{ 程序段头在 Linux 内核源码中常被称作程序头（Program Header）。
} 中，程序段头连续存放，形成程序段头表（Program Segment Header Table）。
实际的 ELF 文件中，为了保留尽可能多的信息，链接视图下仍旧保留程序段头表，执行视图下仍旧保留节头表。
在执行视图下，节头表并没有什么实际用处，解析可执行文件只需要使用程序段头表。

\vspace{2em}
\begin{figure}[h!]
    \caption{ELF 格式}
    \label{ELF_FORMAT}
    \begin{center}
        \newcommand{\multirowcell}[3]
        {
          \multirow{#1}{*}{{\begin{tabular}{>{#2}c}#3\end{tabular}}}
        }
        \begin{tabularx}{12cm}{|*{4}{>{\Centering\tiny\arraybackslash}X|}}
          \hline
          \multicolumn{2}{|c|}{可重定位文件（链接前）} & \multicolumn{2}{c|}{可执行文件（链接后）} \\
          \hline
          \multicolumn{2}{|c|}{ELF 头部} & \multicolumn{2}{c|}{ELF 头部}\\*
          \hline

          \multirowcell{3}{\tiny\bfseries}{程序段头表} & \multirowcell{3}{\tiny}{无实际作用} & \multirowcell{3}{\tiny\bfseries}{程序段头表} & \multirowcell{1}{\tiny}{程序段头 $0$} \\*
          \cline{4-4}
          & & & \multirowcell{1}{\tiny}{\vdots} \\*
          \cline{4-4}
          & & & \multirowcell{1}{\tiny}{程序段头 $n$}\\
          \hline

          \multirowcell{5}{\tiny\bfseries}{节表} & \multirowcell{1}{\tiny}{节 $0$} & \multirowcell{5}{\tiny\bfseries}{程序段表} & \multirowcell{2}{\tiny}{程序段 $0$} \\*
          \cline{2-2}

          & \multirowcell{3}{\tiny}{\vdots} & & \\*
          \cline{4-4}

          & & & \multirowcell{1}{\tiny}{\vdots} \\*
          \cline{4-4}

          & & & \multirowcell{2}{\tiny}{程序段 $n$} \\%\multirow{2}{*}{程序段 $n$} \\*
          \cline{2-2}

          & \multirowcell{1}{\tiny}{节 $m$} & & \\
          \hline

          \multirowcell{3}{\tiny\bfseries}{节头表} & \multirowcell{1}{\tiny}{节 $0$} & \multirowcell{3}{\tiny\bfseries}{节头表} & \multirowcell{3}{\tiny}{无实际作用} \\*
          \cline{2-2}
          & \multirowcell{1}{\tiny}{\vdots} & & \\*
          \cline{2-2}
          & \multirowcell{1}{\tiny}{节 $m$} & & \\
          \hline
        \end{tabularx}
    \end{center}
\end{figure}
\vspace{2em}

%% 下面结合 \lstinline{elf.h}（位于 GNU/Linux 操作系统 \lstinline{/usr/include/} 目录下）中的结构体定义详细分析如何在内存中展开操作系统内核文件。
%
%
%% \begin{minted}{c}
%% typedef struct
%% {
%%     unsigned char e_ident[EI_NIDENT];/* Magic number and other info */
%%     Elf32_Half e_type; /* Object file type */
%%     Elf32_Half e_machine; /* Architecture */
%%     Elf32_Word e_version; /* Object file version */
%%     Elf32_Addr e_entry; /* Entry point virtual address */
%%     Elf32_Off e_phoff; /* Program header table file offset */
%%     Elf32_Off e_shoff; /* Section header table file offset */
%%     Elf32_Word e_flags; /* Processor-specific flags */
%%     Elf32_Half e_ehsize; /* ELF header size in bytes */
%%     Elf32_Half e_phentsize; /* Program header table entry size */
%%     Elf32_Half e_phnum; /* Program header table entry count */
%%     Elf32_Half e_shentsize; /* Section header table entry size */
%%     Elf32_Half e_shnum; /* Section header table entry count */
%%     Elf32_Half e_shstrndx; /* Section header string table index */
%% } Elf32_Ehdr;
%% \end{minted}
%
%% \lstinline{e_ident}，魔数，必须为“ELF”字符串对应的 ASCII 编码。
%
%
%% \lstinline{e_type}，指明了目标文件的类型，如 \lstinline{ET_REL} 表示可重定位文件，\lstinline{ET_EXEC} 表示可执行文件。
%
%
%% \lstinline{e_machine}，指明目标文件体系结构，如 \lstinline{EM_386} 对应 Intel 80386 体系结构，\lstinline{EM_X86_64} 对应 AMD x86\_64 体系结构。
%
%
%% \lstinline{e_version}，目标文件版本。
%
%
%% \lstinline{e_entry}，程序入口的虚拟地址（分页后的虚拟地址），对于上文例子中输出的 SimpleOS.out 可执行文件，也即入口函数 SimpleMain 的地址。
%
%
%% \lstinline{e_phoff}，目标文件中程序段头表的偏移地址。
%
%
%% \lstinline{e_shoff}，目标文件中程序节头表的偏移地址。
%
%
%% \lstinline{e_flags}，处理器标志位。
%
%
%% \lstinline{e_ehsize}，ELF 头部的大小，对于 ELF32 而言一般固定为 52 Bytes。
%
%
%% \lstinline{e_phentsize}，程序段头表每个表项的大小。
%
%
%% \lstinline{e_phnum}，程序段头表的表项数目。
%
%
%% \lstinline{e_shentsize}，程序节头表每个表项的大小。
%
%
%% \lstinline{e_shnum}，程序节头表表项数。
%
%
%% \lstinline{e_shstrndx}，节名称字符串表索引号。
%% 程序员在编写汇编代码时，需要为节指定名称（对于高级语言则由编译器管理）。
%% 这些节的名称会单独保存为一张字符串表，字符串表保存在单独的节（这样的节具有特殊类型 \lstinline{STRTAB}）中，该索引号就对应是该节在节头表中的序号。
%% 通过访问节头表的 \lstinline{e_shstrndx} 号表项，可找到字符串表所在节的信息，访问这个节，就可获取程序中所有节的名称。
%
%
%% 节头（节头表中的表项）定义为如下的结构体。
%
%
%% \begin{minted}{c}
%% typedef struct
%% {
%%     Elf32_Word sh_name; /* Section name (string tbl index) */
%%     Elf32_Word sh_type; /* Section type */
%%     Elf32_Word sh_flags; /* Section flags */
%%     Elf32_Addr sh_addr; /* Section virtual addr at execution */
%%     Elf32_Off sh_offset; /* Section file offset */
%%     Elf32_Word sh_size; /* Section size in bytes */
%%     Elf32_Word sh_link; /* Link to another section */
%%     Elf32_Word sh_info; /* Additional section information */
%%     Elf32_Word sh_addralign; /* Section alignment */
%%     Elf32_Word sh_entsize; /* Entry size if section holds table */
%% } Elf32_Shdr;
%% \end{minted}
%
%% \lstinline{sh_name}，节名在字符串表中的索引号，结合 ELF 头部中的 \lstinline{e_shstrndx} 字段可读取到字符串名称。
%
%
%% \lstinline{sh_type}，节的类型。
%% 如：
%% \lstinline{SHT_PROGBITS} 表示存放程序指令及数据的段（如 \lstinline{.text} 节）；
%% \lstinline{SHT_SYMTAB} 表示符号表；
%% \lstinline{SHT_STRTAB} 表示字符串表；\lstinline{SHT_NOBITS} 表示没有数据的程序段，即 \lstinline{.bss} 节。
%
%
%% \lstinline{sh_flags}，节的标志位，描述了诸如 \lstinline{SHF_WRITE}（可写）、\lstinline{SHF_EXECINSTR}（可执行）等属性。
%
%
%% \lstinline{sh_addr}，节执行时的虚拟地址。
%
%
%% \lstinline{sh_offset}，节在目标文件中的偏移量。
%
%
%% \lstinline{sh_size}，节的大小。
%
%
%% \lstinline{sh_link}，对另一个节的链接。
%
%
%% \lstinline{sh_info}，关于节的额外信息。
%
%
%% \lstinline{sh_addralign}，节按多少字节对齐。
%
%
%% \lstinline{sh_entsize}，如果节中保存的是表，则用于指出每个表项的大小。
%
%
%% 程序段头（程序段头表中的表项）定义为如下结构体。
%
%
%% \begin{minted}{c}
%% typedef struct
%% {
%%     Elf32_Word p_type; /* Segment type */
%%     Elf32_Off p_offset; /* Segment file offset */
%%     Elf32_Addr p_vaddr; /* Segment virtual address */
%%     Elf32_Addr p_paddr; /* Segment physical address */
%%     Elf32_Word p_filesz; /* Segment size in file */
%%     Elf32_Word p_memsz; /* Segment size in memory */
%%     Elf32_Word p_flags; /* Segment flags */
%%     Elf32_Word p_align; /* Segment alignment */
%% } Elf32_Phdr;    
%% \end{minted}
%
%% \lstinline{p_type}，程序段的类型，如 \lstinline{PT_LOAD} 表示可装载的程序段。
%
%
%% \lstinline{p_offset}，程序段在文件映像中的偏移量。
%
%
%% \lstinline{p_vaddr}，程序段的虚拟地址。
%
%
%% \lstinline{p_paddr}，程序段的物理地址，x86 CPU 开启分页后，所有的内存地址都采用虚拟地址，因此 x86 体系结构下的 ELF 目标文件中该项无意义，一般与 \lstinline{p_vaddr} 的值保持一致。
%
%
%% \lstinline{p_filesz}，程序段在文件映像中的大小，即在存储 ELF 目标文件时，程序段实际占用的存储空间。
%
%
%% \lstinline{p_memsz}，程序段装入内存实际需要占用的内存大小。
%
%
%% \lstinline{p_flags}，程序段的属性，如：\lstinline{PF_X} 表示可执行；\lstinline{PF_W} 表示可写。
%
%
%% \lstinline{p_align}，程序段在内存映像和文件映像中按多少字节对齐。
%% \lstinline{0} 或 \lstinline{1} 表示无对齐要求。
%
图 \ref{SimpleOS.out_ELF_HEADER} 是 \lstinline{readelf} 获取到的 \lstinline{SimpleOS.out} 的 ELF 头部信息。
可以获知程序段头表在文件内的偏移为 \Bytes{52}，共有 7 个程序段头。
程序入口地址 \lstinline{SimpleMain} （通过链接器 \lstinline{--entry} 选项指定）为 \lstinline{0xC006'7940}。

\begin{figure}[h!]
\Centering
\caption{\lstinline{SimpleOS.out} ELF 头部}
\label{SimpleOS.out_ELF_HEADER}
\begin{lstlisting}
ELF Header:
Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class:                             ELF32
Data:                              2's complement, little endian
Version:                           1 (current)
OS/ABI:                            UNIX - System V
ABI Version:                       0
Type:                              EXEC (Executable file)
Machine:                           Intel 80386
Version:                           0x1
Entry point address:               0xc0067940
Start of program headers:          52 (bytes into file)
Start of section headers:          22596 (bytes into file)
Flags:                             0x0
Size of this header:               52 (bytes)
Size of program headers:           32 (bytes)
Number of program headers:         7
Size of section headers:           40 (bytes)
Number of section headers:         21
Section header string table index: 19
\end{lstlisting}
\end{figure}

图 \ref{SimpleOS.out_PROGRAM_HEADER} 为文件从磁盘通过前述磁盘驱动读取到内存地址 \lstinline{0xC00E'8000} 处，要正确装载操作系统内核，需要将文件映像中类型非 \lstinline{PT_NULL} 的程序段装载到对应的虚拟地址处。

以装载下表中第二个类型为 \lstinline{PT_LOAD} 的程序段为例，需要从 \lstinline{0xC00E'8000} \lstinline{+} \lstinline{p_offset} \lstinline{(0x00'0AE0)} 处向 \lstinline{p_vaddr} \lstinline{(0xC006'6AE0)} 处连续读取 \lstinline{p_filesz} \lstinline{(0x0'1217)} 字节。

程序段在内存映像中的实际大小（即前文中提到的 \lstinline{p_memsz}）可能会大于文件映像中的大小（\lstinline{p_filesz}），但是 \lstinline{p_vaddr} 总是能保证起始地址的正确性，剩余的内存空间则是未初始化的内存区域，这部分内存空间的正确使用由被装入程序保证。

对于程序运行中不需要的段，如下表中类型为 \lstinline{PT_PHDR} 的字段（程序头表本身，对程序实际运行无作用），对应的程序段被装载到 \lstinline{0xC006'5034} \TO \lstinline{0xC006'5114} 处，随后装载的第一个类型为 \lstinline{PT_LOAD}（范围为 \lstinline{0xC006'5000} \TO \lstinline{0xC006'5AE0}）会覆盖之前装入内存的 \lstinline{PT_PHDR} 程序段。
这样，便可确保程序运行时不需要的的冗余程序段不会真正装入内存。

\begin{figure}[h!]
    \Centering
    \caption{\lstinline{SimpleOS.out} 程序头表}
    \label{SimpleOS.out_PROGRAM_HEADER}
    \lstset {
        basicstyle = \bfseries\zihao{-5}\ttfamily\linespread{1.5}
    }
    \begin{lstlisting}
Program Headers:
Type      Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
PHDR      0x000034 0xc0065034 0xc0065034 0x000e0 0x000e0 R   0x4
LOAD      0x000000 0xc0065000 0xc0065000 0x00ae0 0x00ae0 R   0x1000
LOAD      0x000ae0 0xc0066ae0 0xc0066ae0 0x01217 0x01217 R E 0x1000
LOAD      0x001cf8 0xc0068cf8 0xc0068cf8 0x00008 0x00308 RW  0x1000
LOAD      0x001d00 0xc0069d00 0xc0069d00 0x0000c 0x00810 RW  0x1000
GNU_RELRO 0x001cf8 0xc0068cf8 0xc0068cf8 0x00008 0x00308 R   0x1
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0
    \end{lstlisting}
\end{figure}

\lstset {
    basicstyle = \bfseries\zihao{5}\ttfamily\linespread{1.5}
}

根据程序段头表的内容正确装入各个段后，需要找到程序的入口地址。
入口地址在 ELF 头的 \lstinline{e_entry} 字段中指出。
在装载完成后，只要通过跳转指令跳转到入口处即可运行操作系统内核程序。
至此，引导加载器就将控制权完全交付操作系统了。
